package com.lonefennec.chameleon;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.EdgeShape;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.ObjectMap;

public class Level extends Group
{
	// definition of tile types read from the tilEd file
	private Map<Integer, Texture> tileTypes = new HashMap<Integer, Texture>();
	
	// the layout of the map
	private Map<Point, Integer> tiles = new HashMap<Point, Integer>();
	
	private final List<Vector2> playerStarts = new ArrayList<Vector2>();
	private final List<Rectangle> guardPatrols = new ArrayList<Rectangle>();
	private Rectangle exit;
	
	private Body body;
	
	public Vector2 getPlayerStart(int index) throws IndexOutOfBoundsException
	{
		return playerStarts.get(index);
	}
	
	public int getPlayerStartCount()
	{
		return playerStarts.size();
	}
	
	public Rectangle getGuardPatrol(int index) throws IndexOutOfBoundsException	
	{
		return guardPatrols.get(index);
	}
	
	public int getGuardPatrolCount()
	{
		return guardPatrols.size();
	}
	
	public Rectangle getExit()
	{
		return exit;
	}
	
	@Override
	public void draw(SpriteBatch batch, float parentAlpha)
	{
		for (Point coord : tiles.keySet())
		{
			Integer tile = tiles.get(coord);
			Texture texture = tileTypes.get(tile);
			
			float drawX = coord.x * tileWidth;
			float drawY = coord.y * tileHeight;
			
			batch.draw(texture, drawX, drawY, tileWidth, tileHeight);
		}
		
		super.draw(batch, parentAlpha);
	}
	
	@Override
	public Actor hit(float x, float y)
	{
		return super.hit(x, y);
	}
	
	// width of one tile
	private float tileWidth;
	
	// height of one tile
	private float tileHeight;
	
	// width in tiles
	private int mapWidth;
	
	// height in tiles
	private int mapHeight;
	
	private Vector2 mapCoordToWorld(Vector2 coord)
	{
		Vector2 result = coord.cpy();
		
		float totalHeight = mapHeight * tileHeight * Physics.WORLD_TO_SCREEN;
		
		result.y = totalHeight - result.y;
		
		return result.mul(Physics.SCREEN_TO_WORLD);
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private Rectangle getObjectRectFromJson(ObjectMap jsonData)
	{
		Rectangle result = new Rectangle();
		
		Vector2 corner = new Vector2();						
		corner.x = (Float) jsonData.get("x");
		corner.y = (Float) jsonData.get("y");
		corner = mapCoordToWorld(corner);
		
		result.width = (Float) jsonData.get("width") * Physics.SCREEN_TO_WORLD;
		result.height = (Float) jsonData.get("height") * Physics.SCREEN_TO_WORLD;
		
		result.x = corner.x;
		result.y = corner.y;
		
		return result;
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void loadLevel(String path) throws Exception
	{
		//clear the previously loaded level if any
		tileTypes.clear();
		tiles.clear();
		playerStarts.clear();
		guardPatrols.clear();
		exit = null;
		
		if (body != null)
			Physics.getWorld().destroyBody(body);
		
		ObjectMap<String, ?> file = new Json().fromJson(ObjectMap.class,
				Gdx.files.internal(path).readString());
		
		mapWidth = ((Float) file.get("width")).intValue();
		mapHeight = ((Float) file.get("height")).intValue();
		
		//these are in pixels when loaded - convert to world coords
		tileWidth = ((Float) file.get("tilewidth")) * Physics.SCREEN_TO_WORLD;
		tileHeight = ((Float) file.get("tileheight")) * Physics.SCREEN_TO_WORLD;
		
		Array<ObjectMap> tileSets = (Array) file.get("tilesets");
		
		// load info about tile types from the tilEd file
		int tileSetCounter = 1;
		for (ObjectMap tileSet : tileSets)
		{
			String tileImage = (String) tileSet.get("image");
			int fileNameStart = tileImage.lastIndexOf('/');
			String fileNameOnly = tileImage
					.substring(fileNameStart > 0 ? fileNameStart : 0);
			String fullFilePath = "tiles/" + fileNameOnly;

			ChameleonGame.assets.load(fullFilePath, Texture.class);
			ChameleonGame.assets.finishLoading();
			
			Texture newTexture = ChameleonGame.assets.get(fullFilePath, Texture.class);
			
			tileTypes.put(Integer.valueOf(tileSetCounter), newTexture);
			
			++tileSetCounter;
		}
		
		// load data from the layers
		Array layers = (Array) file.get("layers");
		
		//for now, only support a single tile layer - if more are found, ignore them
		boolean tileLayerLoaded = false;
		
		for (Object layerObj : layers)
		{
			ObjectMap layer = (ObjectMap) layerObj;
			
			String layerType = (String) layer.get("type");
			
			if (layerType.equalsIgnoreCase("tilelayer")
				&& tileLayerLoaded == false)
			{
				//the "data" element of the layer is an array of tile type IDs
				Array firstLayerTiles = (Array) layer.get("data");
				
				for (int mapX = 0; mapX < mapWidth; ++mapX)
				{
					for (int mapY = 0; mapY < mapHeight; ++mapY)
					{
						/*
						 * tilEd exports the level with the origin as the top-left,
						 * while the stage uses the bottom-left as the origin - so flip
						 * this here
						 */
						int flippedY = mapHeight - (mapY + 1);
						
						//uses the original y here because this is the map's own coordinates
						Point mapCoord = new Point(mapX, mapY);
						
						int tileIndex = flippedY * mapWidth + mapX;
						
						Array<Float> layerTiles = (Array<Float>) firstLayerTiles;
						int tileType = layerTiles.get(tileIndex).intValue();
						
						// the tile list is a sparse map, so only include non-empty
						// tiles (0 == empty)
						if (tileType != 0)
							tiles.put(mapCoord, tileType);
					}
				}
				
				tileLayerLoaded = true;
			}
			else if (layerType.equalsIgnoreCase("objectgroup"))
			{
				Array layerObjects = (Array) layer.get("objects");
				
				for (Object object : layerObjects)
				{
					ObjectMap objMap = (ObjectMap) object;
					
					String objName = (String) objMap.get("name");
					
					if (objName.equalsIgnoreCase("PlayerStart"))
					{
						Vector2 newStartPos = new Vector2();
						
						//total height is in world coords so convert it
						float totalHeight = mapHeight * tileHeight * Physics.WORLD_TO_SCREEN;
						
						newStartPos.x = (Float) objMap.get("x");
						newStartPos.y = (Float) objMap.get("y");
						
						//we want to spawn the player at the CENTER of the object, not the origin
						float objWidth = (Float) objMap.get("width");
						float objHeight = (Float) objMap.get("height");
						
						newStartPos.x += objWidth / 2;
						newStartPos.y += objHeight / 2;
						
						//need to flip this again - different up directions
						newStartPos.y = totalHeight - newStartPos.y;
						
						//the TilED coords are in pixels - convert to meters here
						newStartPos.x *= Physics.SCREEN_TO_WORLD;
						newStartPos.y *= Physics.SCREEN_TO_WORLD;
						
						playerStarts.add(newStartPos);
					}
					else if (objName.equalsIgnoreCase("GuardPatrol"))
					{
						guardPatrols.add(getObjectRectFromJson(objMap));
						
					}
					else if (objName.equalsIgnoreCase("Exit"))
					{
						exit = getObjectRectFromJson(objMap);						
					}
				}
			}
		}
		
		BodyDef tileBodyDef = new BodyDef();
		tileBodyDef.type = BodyType.StaticBody;
		tileBodyDef.position.set(0, 0);
		
		body = Physics.getWorld().createBody(tileBodyDef);
		
		//add each tile to the physics world
		for (Point coord : tiles.keySet())
		{
			float left = coord.x * tileWidth;
			float top = coord.y * tileHeight;
			
			float right = left + tileWidth;
			float bottom = top + tileHeight;
			
			//using edge shapes for improved collision detection - break the rect up into its 4 edges
			Vector2[] edges = new Vector2[]
				{
					new Vector2(left, top),
					new Vector2(right, top),
					new Vector2(right, bottom),
					new Vector2(left, bottom),
				};
			
			EdgeShape edge = new EdgeShape();
			FixtureDef tileFix = new FixtureDef();
			tileFix.friction = 0.0f;
			tileFix.shape = edge;
			
			edge.set(edges[1], edges[0]);
			body.createFixture(tileFix);
			
			edge.set(edges[2], edges[1]);
			body.createFixture(tileFix);
			
			edge.set(edges[3], edges[2]);
			body.createFixture(tileFix);
			
			edge.set(edges[0], edges[3]);
			body.createFixture(tileFix);
		}
		
		body.setUserData(this);
	}
	
	public Level()
	{
		super();
	}
}
